package top.z.template.parse.ast.expression;

import top.z.template.Context;
import top.z.template.Error;
import top.z.template.Template;
import top.z.template.interpreter.Reflection;
import top.z.template.parse.Ast.Expression;
import top.z.template.parse.Span;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;

/**
 * 调用对象的方法(例如: object.method(a, b, c))
 */
public class MethodCall extends Expression {

    private final MemberAccess          method;
    private final List<Expression>      argument;
    private       Object                cacheMethod;
    private final ThreadLocal<Object[]> cacheArgument;

    public MethodCall(Span span, MemberAccess method, List<Expression> argument) {
        super(span);
        this.method = method;
        this.argument = argument;
        this.cacheArgument = new ThreadLocal<>();
    }

    public Expression getObject() {
        return method.getObject();
    }

    public MemberAccess getMethod() {
        return method;
    }

    public List<Expression> getArgument() {
        return argument;
    }

    public Object getCacheMethod() {
        return cacheMethod;
    }

    public void setCacheMethod(Object cacheMethod) {
        this.cacheMethod = cacheMethod;
    }

    public Object[] getCacheArgument() {
        Object[] args = cacheArgument.get();
        if (args == null) {
            args = new Object[argument.size()];
            cacheArgument.set(args);
        }
        return args;
    }

    /**
     * 必须在此节点完成计算时调用, 以免泄漏内存
     */
    public void clearCachedArguments() {
        Object[] args = getCacheArgument();
        Arrays.fill(args, null);
    }

    @Override
    public Object evaluate(Template template, Context context, OutputStream out) throws IOException {
        try {
            Object object = getObject().evaluate(template, context, out);
            if (object == null) {
                throw Error.error("Couldn't find object in context.", getSpan());
            }

            Object[]         argumentValue = getCacheArgument();
            List<Expression> argument      = getArgument();
            for (int index = 0, n = argumentValue.length; index < n; index++) {
                Expression expr = argument.get(index);
                argumentValue[index] = expr.evaluate(template, context, out);
            }

            // if the object we call the method on is a Macros instance, lookup the macro by name and execute its node list
            /*if (object instanceof Macro) {
                Map<String, Macro> macroMap = (Map<String, Macro>) object;
                Macro              macro    = macroMap.get(getMethod().getName().getText());
                if (macro != null) {
                    if (macro.getArgumentName().size() != argument.size()) {
                        Error.error("Expected " + macro.getArgumentName().size() + " argument, got " + argument.size(), getSpan());
                    }
                    Context macroContext = macro.getContext();

                    // Set all included macros on the macro's context
                    for (String variable : context.getVariable()) {
                        Object value = context.get(variable);
                        if (value instanceof Macro) {
                            macroContext.set(variable, value);
                        }
                    }

                    // Set argument
                    for (int i = 0; i < argument.size(); i++) {
                        Object arg  = argumentValue[i];
                        String name = macro.getArgumentName().get(i).getText();
                        macroContext.set(name, arg);
                    }
                    Object result = AstInterpreter.interpretNodeList(macro.getBody(), macro.getTemplate(), macroContext, out);
                    if (result == Return.RETURN_SENTINEL) {
                        return ((Return.ReturnValue) result).getValue();
                    } else {
                        return null;
                    }
                }
            }*/

            // Otherwise try to find a corresponding method or field pointing to a lambda.
            Object method = getCacheMethod();
            if (method != null) {
                try {
                    return Reflection.getInstance().callMethod(object, method, argumentValue);
                } catch (Throwable t) {
                    // fall through
                }
            }

            method = Reflection.getInstance().getMethod(object, getMethod().getName().getText(), argumentValue);
            if (method != null) {
                // found the method on the object, call it
                setCacheMethod(method);
                try {
                    return Reflection.getInstance().callMethod(object, method, argumentValue);
                } catch (Throwable t) {
                    Error.error(t.getMessage(), getSpan(), t);
                    return null; // never reached
                }
            } else {
                // didn't find the method on the object, try to find a field pointing to a lambda
                Object field = Reflection.getInstance().getField(object, getMethod().getName().getText());
                if (field == null) {
                    throw Error.error("Couldn't find method '" + getMethod().getName().getText() + "' for object of type '" + object.getClass().getSimpleName() + "'.", getSpan());
                }
                Object function = Reflection.getInstance().getFieldValue(object, field);
                method = Reflection.getInstance().getMethod(function, null, argumentValue);
                if (method == null) {
                    throw Error.error("Couldn't find function in field '" + getMethod().getName().getText() + "' for object of type '" + object.getClass().getSimpleName() + "'.", getSpan());
                }
                try {
                    return Reflection.getInstance().callMethod(function, method, argumentValue);
                } catch (Throwable t) {
                    throw Error.error(t.getMessage(), getSpan(), t);
                }
            }
        } finally {
            clearCachedArguments();
        }
    }
}